package IC.SemanticChecks;

import IC.Constants;
import IC.UnaryOps;
import IC.AST.*;
import IC.SymbolTable.SymbolTable;
import IC.Types.ArrayType;
import IC.Types.ClassType;
import IC.Types.MethodType;
import IC.Types.Type;
import IC.Types.TypeTable;

/**
 * A visitor class that is used to run all of the type checks.	
 */
public class TypeCheckingVisitor implements Visitor {
	
	//C'tor
	public TypeCheckingVisitor(){
				
	}
		
	public Object visit(Program program) {
		for (ICClass c : program.getClasses()) 
			c.accept(this);
		return null;
	}

	
	public Object visit(ICClass icClass) {
		for (Method m : icClass.getMethods())
			m.accept(this);
		for (Field f : icClass.getFields())
			f.accept(this);
		return null;
	}

	
	public Object visit(Field field) {
		field.setNodeType((Type) field.getType().accept(this));
		return null;
	}

	
	public Object visit(VirtualMethod method) {
		method.getType().accept(this);
		for (Formal f : method.getFormals()) 
			f.accept(this);
		for (Statement s : method.getStatements()) 
			s.accept(this);
		return null;
	}

	
	public Object visit(StaticMethod method) {
		method.getType().accept(this);
		for (Formal f : method.getFormals())
			f.accept(this);
		for (Statement s : method.getStatements())
			s.accept(this);
		return null;
	}

	
	public Object visit(LibraryMethod method) {
		method.getType().accept(this);
		for (Formal f : method.getFormals())
			f.accept(this);
		return null;
	}

	
	public Object visit(Formal formal) {
		Type res = (Type) formal.getType().accept(this);
		formal.setNodeType(res);
		return res;
	}

	
	public Object visit(PrimitiveType type) {
		Type result = null;
		switch (type.getDataType()) {
			case INT:
				result = TypeTable.intType;
				break;
			case BOOLEAN:
				result = TypeTable.boolType;
				break;
			case STRING:
				result = TypeTable.stringType;
				break;
			case VOID:
				result = TypeTable.voidType;
				break;
		}
		Type t = TypeTable.getArrayType(result, type.getDimension(), type.getLine());
		type.setNodeType(t);
		return t;
	}

	
	public Object visit(UserType type) {
		Type classType = TypeTable.classType(type.getName());
		Type t = TypeTable.getArrayType(classType, type.getDimension(), type.getLine());
		type.setNodeType(t);
		return t;
	}

	
	public Object visit(Assignment assignment) {
		Type varType = (Type) assignment.getVariable().accept(this);
		Type assType = (Type) assignment.getAssignment().accept(this);
		if (varType == null || assType == null) 
			return null;
		if (!assType.InstanceOf(varType)) 
			SemanticError.handle(SemanticErrorType.INVALID_ASSIGNMENT, assignment.getLine());
		return null;
	}

	
	public Object visit(CallStatement callStatement) {
		callStatement.getCall().accept(this);
		return null;
	}

	
	public Object visit(Return returnStatement) {
		Type expectedType = returnStatement.getScope().lookup(Constants.RETURN).getType();
		if (returnStatement.hasValue()) {
			Type retType = (Type) returnStatement.getValue().accept(this);
			returnStatement.setNodeType(retType);
			if (retType != null && !retType.InstanceOf(expectedType)) {
				SemanticError.handle(SemanticErrorType.INVALID_RETURN_TYPE, returnStatement.getLine());
			}
		} else {
			if (!expectedType.equals(TypeTable.voidType)) {
				SemanticError.handle(SemanticErrorType.INVALID_RETURN_TYPE, returnStatement.getLine());
			}
		}
		return null;
	}

	
	public Object visit(If ifStatement) {
		Type conditionType = (Type) ifStatement.getCondition().accept(this);
		if (conditionType == null || conditionType != TypeTable.boolType) {
			SemanticError.handle(SemanticErrorType.INVALID_IF_CONDITION, ifStatement.getLine());
		}
		ifStatement.getOperation().accept(this);
		if (ifStatement.hasElse()) {
			ifStatement.getElseOperation().accept(this);
		}
		return null;
	}

	
	public Object visit(While whileStatement) {
		Type conditionType = (Type) whileStatement.getCondition().accept(this);
		if (conditionType == null || conditionType != TypeTable.boolType) {
			SemanticError.handle(SemanticErrorType.INVALID_WHILE_CONDITION, whileStatement.getLine());
		}
		whileStatement.getOperation().accept(this);
		return null;
	}

	
	public Object visit(Break breakStatement) {
		return null;
	}

	
	public Object visit(Continue continueStatement) {
		return null;
	}

	
	public Object visit(StatementsBlock statementsBlock) {
		for (Statement s : statementsBlock.getStatements())
			s.accept(this);
		return null;
	}

	
	public Object visit(LocalVariable localVariable) {
		Type initType = null;
		if (localVariable.hasInitValue()) {
			initType = (Type) localVariable.getInitValue().accept(this);
		}
		Type varType = (Type) localVariable.getType().accept(this);
		localVariable.setNodeType(varType);
        if (initType != null && !initType.InstanceOf(varType)) { 
        	SemanticError.handle(SemanticErrorType.INVALID_LOCAL_VAR_INIT_TYPE, localVariable.getLine());
        }
		return null;
	}

	
	public Object visit(VariableLocation location) {
		if (location.isExternal()) {
			Type locationType = (Type) location.getLocation().accept(this);
			if (locationType == null) {
				return null;
			}
			if (!locationType.isClass()) {
				SemanticError.handle(SemanticErrorType.INVALID_CLASS_TYPE, location.getLine());
			} else {
				Type t = ((ClassType)locationType).getAst().getScope().virtualLookup(location.getName()).getType();
				location.setNodeType(t);
				return t;
			}
		} else {
			Type t = location.getScope().lookup(location.getName(), location.getAppearanceOrder()).getType();
			location.setNodeType(t);
			return t;
		}
		return null;
	}

	
	public Object visit(ArrayLocation location) {
		Type arrayType = (Type) location.getArray().accept(this);
		Type indexType = (Type) location.getIndex().accept(this);
		if (arrayType == null || indexType == null) {
			return null;
		}
		if (!arrayType.isArray()) {
			SemanticError.handle(SemanticErrorType.INVALID_ARRAY_TYPE, location.getLine());
		} else if (indexType != TypeTable.intType) {
			SemanticError.handle(SemanticErrorType.INVALID_ARRAY_INDEX, location.getLine());
		} else {
			location.setNodeType(((ArrayType)arrayType).getInnerType());
			return ((ArrayType)arrayType).getInnerType();
		}
		return null;
	}

	
	public Object visit(StaticCall call) {
		SymbolTable holdingClass = TypeTable.getClass(call.getClassName());
		MethodType methodType = (MethodType) holdingClass.staticLookup(call.getName()).getType();
		if (call.getArguments().size() != methodType.getFormalTypes().length) {
			SemanticError.handle(SemanticErrorType.INVALID_ARGUMENTS_AMOUNT, call.getLine());
		} else {
			int i = 0;
			for (Expression arg : call.getArguments()) {
				Type paramType = methodType.getFormalTypes()[i];
				Type argType = (Type) arg.accept(this);
				if (argType == null) {
					return null;
				}
				if (!argType.InstanceOf(paramType)) {
					SemanticError.handle(SemanticErrorType.INVALID_ARGUMENT_TYPE, call.getLine());
				}
				i++;
			}
		}
		call.setNodeType(methodType.getReturnType());
		return methodType.getReturnType();
	}

	
	public Object visit(VirtualCall call) {
		MethodType methodType;
		if (call.isExternal()) {
			ClassType classType = (ClassType) call.getLocation().accept(this);
			methodType = (MethodType) classType.getAst().getScope().virtualLookup(call.getName()).getType();
		} else {
			methodType = (MethodType) call.getScope().virtualLookup(call.getName()).getType();
		}
		if (call.getArguments().size() != methodType.getFormalTypes().length) {
			SemanticError.handle(SemanticErrorType.INVALID_ARGUMENTS_AMOUNT, call.getLine());
		} else {
			int i = 0;
			for (Expression arg : call.getArguments()) {
				Type paramType = methodType.getFormalTypes()[i];
				Type argType = (Type) arg.accept(this);
				if (argType == null) {
					return null;
				}
				if (!argType.InstanceOf(paramType)) {
					SemanticError.handle(SemanticErrorType.INVALID_ARGUMENT_TYPE, call.getLine());
				}
				i++;
			}
		}
		call.setNodeType(methodType.getReturnType());
		return methodType.getReturnType();
	}

	
	public Object visit(This thisExpression) {
		Type thisType = thisExpression.getScope().virtualLookup(Constants.THIS).getType();
		thisExpression.setNodeType(thisType);
		return thisType;
	}

	
	public Object visit(NewClass newClass) {
		newClass.setNodeType(TypeTable.classType(newClass.getName()));
		return TypeTable.classType(newClass.getName());
	}

	
	public Object visit(NewArray newArray) {
		Type newArrayType = (Type) newArray.getType().accept(this);
		Type sizeType = (Type) newArray.getSize().accept(this);
		if (newArrayType == null || sizeType == null) {
			return null;
		}
		if (sizeType == TypeTable.intType && newArrayType.isSuitForArray()) {
			newArray.setNodeType(TypeTable.arrayType(newArrayType));
			return TypeTable.arrayType(newArrayType);
		}
		SemanticError.handle(SemanticErrorType.INVALID_ARRAY_LENGTH, newArray.getLine());
		return null;
	}

	
	public Object visit(Length length) {
		Type arrayType = (Type) length.getArray().accept(this);
		if (arrayType == null) {
			return null;
		}
		if (arrayType.isArray()) {
			length.setNodeType(TypeTable.intType);
			return TypeTable.intType;
		}
		SemanticError.handle(SemanticErrorType.INVALID_ARRAY_TYPE, length.getLine());
		return null;
	}

	
	public Object visit(MathBinaryOp binaryOp) {
		Type firstOperandType = (Type) binaryOp.getFirstOperand().accept(this);
		Type secondOperandType = (Type) binaryOp.getSecondOperand().accept(this);
		if (firstOperandType == null || secondOperandType == null) {
			return null;
		}
		switch (binaryOp.getOperator()) {
		case PLUS:
			if (firstOperandType == TypeTable.stringType && secondOperandType == TypeTable.stringType) {
				binaryOp.setNodeType(TypeTable.stringType);
				return TypeTable.stringType;
			}
		case DIVIDE:
		case MINUS:
		case MOD:
		case MULTIPLY:
			if (firstOperandType == TypeTable.intType && secondOperandType == TypeTable.intType) {
				binaryOp.setNodeType(TypeTable.intType);
				return TypeTable.intType;
			}
		}
		SemanticError.handle(SemanticErrorType.INVALID_MATH_BINARY_OP, binaryOp.getLine());
		return null;
	}

	
	public Object visit(LogicalBinaryOp binaryOp) {
		Type firstOperandType = (Type) binaryOp.getFirstOperand().accept(this);
		Type secondOperandType = (Type) binaryOp.getSecondOperand().accept(this);
		if (firstOperandType == null || secondOperandType == null) {
			return null;
		}
		switch (binaryOp.getOperator()) {
		case GT:
		case GTE:
		case LT:
		case LTE:
			if (firstOperandType == TypeTable.intType && secondOperandType == TypeTable.intType) {
				binaryOp.setNodeType(TypeTable.boolType);
				return TypeTable.boolType;
			}
			break;
		case EQUAL:
		case NEQUAL:
			if (firstOperandType.InstanceOf(secondOperandType) || secondOperandType.InstanceOf(firstOperandType)) {
				binaryOp.setNodeType(TypeTable.boolType);
				return TypeTable.boolType;
			}
			break;
		case LAND:
		case LOR:
			if (firstOperandType == TypeTable.boolType && secondOperandType == TypeTable.boolType) {
				binaryOp.setNodeType(TypeTable.boolType);
				return TypeTable.boolType;
			}
		}
		SemanticError.handle(SemanticErrorType.INVALID_LOGICAL_BINARY_OP, binaryOp.getLine());
		return null;
	}

	
	public Object visit(MathUnaryOp unaryOp) {
		Type operandType = (Type) unaryOp.getOperand().accept(this);
		if (operandType == null) {
			return null;
		}
		if (unaryOp.getOperator().equals(UnaryOps.UMINUS) && operandType == TypeTable.intType) {
			unaryOp.setNodeType(TypeTable.intType);			
			return TypeTable.intType;
		} else {
			SemanticError.handle(SemanticErrorType.INVALID_MATH_UNARY_OP, unaryOp.getLine());
		}
		return null;
	}
	
	
	public Object visit(LogicalUnaryOp unaryOp) {
		Type operandType = (Type) unaryOp.getOperand().accept(this);
		if (operandType == null) {
			return null;
		}
		if (unaryOp.getOperator().equals(UnaryOps.LNEG) && operandType == TypeTable.boolType) {
			unaryOp.setNodeType(TypeTable.boolType);
			return TypeTable.boolType;
		} else {
			SemanticError.handle(SemanticErrorType.INVALID_LOGICAL_UNARY_OP, unaryOp.getLine());
		}
		return null;
	}

	
	public Object visit(Literal literal) {
		switch (literal.getType()) {
		case INTEGER:
			literal.setNodeType(TypeTable.intType);
			return TypeTable.intType;
		case STRING:
			literal.setNodeType(TypeTable.stringType);
			return TypeTable.stringType;
		case NULL:
			literal.setNodeType(TypeTable.nullType);
			return TypeTable.nullType;
		case TRUE:
		case FALSE:
			literal.setNodeType(TypeTable.boolType);
			return TypeTable.boolType;
		}
		SemanticError.handle(SemanticErrorType.INVALID_LITERAL,literal.getLine());
		return null;
	}

	
	public Object visit(ExpressionBlock expressionBlock) {
		Type exprType = (Type) expressionBlock.getExpression().accept(this);
		expressionBlock.setNodeType(exprType);	
		return exprType;
	}

}
